package senzuri.acceleraudio.data;

import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.ShortBuffer;

import senzuri.acceleraudio.R;
import senzuri.acceleraudio.util.AsyncTask;
import senzuri.acceleraudio.util.InfiniteBuffer;

public abstract class MusicGenerator extends AsyncTask {
  public final MusicSession ms;
  
  public MusicGenerator(MusicSession ms) {
    this.ms = ms;
  }
  
  @Override public void doInBackground() throws IOException {
    MusicSession.Parameters parameters = ms.parameters;
    
    File micData = Database.getInstance(null).getFile(ms.data + ".pcm");
    File locData = Database.getInstance(null).getFile(ms.data + ".loc");
    File accData = Database.getInstance(null).getFile(ms.data + ".acc");
    
    if (micData == null) {
      throw new IOException("File \"" + ms.data + ".pcm\" not found");
    }

    int micLength = (int)micData.length(); // numero di byte nel file
    short[] music = new short[micLength / 2]; // numero di campioni nel file
    
    if (parameters.useMicrophone)
      generateMicrophoneMusic(micData, music, parameters.samplingRate);
    
    if (Thread.interrupted())
      return;
    
    if (parameters.useGPS && locData != null)
      generateLocationMusic(locData, music, parameters.samplingRate);
    
    if (Thread.interrupted())
      return;
    
    if (parameters.useAccelerometer() && accData != null)
      generateAccelerometerMusic(accData, music, parameters.samplingRate,
          parameters.useAccelerometerX, parameters.useAccelerometerY, parameters.useAccelerometerZ);

    if (Thread.interrupted())
      return;

    File wavOutput = Database.getInstance(null).createFile(ms.id + ".wav");
    if (wavOutput == null)
      throw new IOException("Couldn't create output file");
    outputWavFile(wavOutput, music, parameters.samplingRate);
    
    return;
  }
  
  private void publishProgress(int step, int position, int total) {
    final int[] descriptions = {R.string.reading, R.string.generating, R.string.generating, R.string.generating, R.string.writing};
    final float[] weights = {.2f, .3f, .2f, .2f, .1f};
    final float[] cumulative = {0f, .2f, .5f, .7f, .9f};
    
    int description = descriptions[step];
    publishProgress(new Object[] {position * weights[step] / total + cumulative[step], description});
  }
  
  private void generateMicrophoneMusic(File micData, short[] music, int samplingRate) throws IOException {
    publishProgress(0, 0, 1);

    short[] temp = new short[8000];
    ByteBuffer buffer = ByteBuffer.allocate(music.length * 2);
    DataInputStream micInput = new DataInputStream(new FileInputStream(micData));
    
    int read = micInput.read(buffer.array());
    
    micInput.close();
    
    if (read != music.length * 2)
      throw new IOException("Read the wrong number of bytes");
    
    buffer.rewind();
    ShortBuffer musicBuffer = buffer.asShortBuffer();
    
    int i = 0;
    while (musicBuffer.position() < music.length) {
      int toRead = Math.min(temp.length, music.length - musicBuffer.position());
      musicBuffer.get(temp, 0, toRead);
      System.arraycopy(temp, 0, music, i, toRead);
      i += toRead;
      publishProgress(0, i, music.length);
    }
  }

  private void generateLocationMusic(File locData, short[] music, int samplingRate) throws IOException {
    final int amp = 5000;
    publishProgress(1, 0, 1);
    
    InfiniteBuffer.Long locationValuesTimestamps = new InfiniteBuffer.Long(100);
    InfiniteBuffer.Double locationValues0 = new InfiniteBuffer.Double(100);
    InfiniteBuffer.Double locationValues1 = new InfiniteBuffer.Double(100);
    InfiniteBuffer.Double locationValues2 = new InfiniteBuffer.Double(100);
    DataInputStream locInput = new DataInputStream(new FileInputStream(locData));
    
    while (true) {
      try {
        locationValuesTimestamps.add(locInput.readLong());
        locationValues0.add(locInput.readDouble());
        locationValues1.add(locInput.readDouble());
        locationValues2.add(locInput.readDouble());
      } catch (EOFException ignored) {
        break;
      }
    }
    
    locInput.close();
    
    if (locationValuesTimestamps.size() == 0)
      return;
    
    short[] values = new short[locationValues2.size()];
    for (int i = 0; i < locationValues0.size(); i++)
      values[i] = (short)((((int)(locationValues0.get(i) * 1000000)) % 42 - 21) * 600);
    
    int i = 0;
    int p = 0;
    while (i < music.length) {
      long t = i * 1000000000L / samplingRate;
      for (; p < locationValuesTimestamps.size() - 1 && locationValuesTimestamps.get(p+1) < t; p++);
      double lat = locationValues0.get(p);
      double lon = locationValues1.get(p);
      // durata del rumore generata da latitudine
      int d = (((int)(lat * 1000000)) % 31 + 5) * 100;
      short prev = 0;
      // questo ciclo for scrive il rumore nel vettore music
      for (int n = 0; n < d; n++) {
        if (i >= music.length)
          break;
        // rumore bianco smorzato linearmente
        short drum = (short)((Math.random() * amp - amp / 2) * (d - n) / d);
        prev += (drum - prev) / 5;
        
        music[i] = clip((int)music[i] + (int)drum);
        i++;
      }
      // aggiungo un silenzio generato da longitudine
      i += (((int)(lon * 1000000)) % 13 + 2) * 100;
      
      publishProgress(1, i, music.length);
    }
  }

  private void generateAccelerometerMusic(File accData, short[] music, int samplingRate, boolean useX, boolean useY, boolean useZ) throws IOException {
    final int ampX = 3000;
    final int ampYZ = 10000;
    publishProgress(2, 0, 1);
    
    InfiniteBuffer.Long accelerometerValuesTimestamps = new InfiniteBuffer.Long(1000);
    InfiniteBuffer.Float accelerometerValues0 = new InfiniteBuffer.Float(1000);
    InfiniteBuffer.Float accelerometerValues1 = new InfiniteBuffer.Float(1000);
    InfiniteBuffer.Float accelerometerValues2 = new InfiniteBuffer.Float(1000);
    DataInputStream accInput = new DataInputStream(new FileInputStream(accData));
    
    while (true) {
      try {
        accelerometerValuesTimestamps.add(accInput.readLong());
        accelerometerValues0.add(accInput.readFloat());
        accelerometerValues1.add(accInput.readFloat());
        accelerometerValues2.add(accInput.readFloat());
      } catch (EOFException ignored) {
        break;
      }
    }
    
    accInput.close();
    
    if (accelerometerValuesTimestamps.size() == 0)
      return;
    
    // circa un modulatore di frequenza per una onda quadra
    short v = 0;
    int p = 0;
    if (useX)
      for (int i = 0; i < music.length; i++) {
        long t = i * 1000000000L / samplingRate;
        for (; p < accelerometerValuesTimestamps.size() - 1 && accelerometerValuesTimestamps.get(p+1) < t; p++);
        float x = accelerometerValues0.get(p);
        v += (int)(x * 2000);
        music[i] = clip(music[i] + (int)(v > 0 ? ampX / 2 : -ampX / 2));
        
        if (i % 1000 == 0)
          publishProgress(2, i, music.length);
      }

    v = 0;
    p = 0;
    publishProgress(3, 0, 1);
    if (useZ || useY) {
      float smoothZ = 0;
      for (int i = 0; i < music.length; i++) {
        long t = i * 1000000000L / samplingRate;
        for (; p < accelerometerValuesTimestamps.size() - 1 && accelerometerValuesTimestamps.get(p+1) < t; p++);
        float y = 0;
        if (useY)
          y = accelerometerValues1.get(p);
        float z = 0;
        if (useZ) {
          float z0 = accelerometerValues2.get(p);
          if (p >= accelerometerValues2.size() - 1) {
            z = z0;
          } else {
            float z1 = accelerometerValues2.get(p + 1);
            long t0 = accelerometerValuesTimestamps.get(p);
            long t1 = accelerometerValuesTimestamps.get(p + 1);
            z = (t - t0) * (z1 - z0) / (t1 - t0) + z0;
          }
        }
        smoothZ += z - smoothZ * 1.5f;
        
        v += (int)((y + 5) * 1000);
        // sinusoide modulata in frequenza
        int s = (int)(Math.sin(v * 9.58738e-5) * ampYZ / 2);
        // modulazione in ampiezza
        s *= 1 - Math.abs(smoothZ / 10);
        music[i] = clip((int)music[i] + s);
        
        if (i % 1000 == 0)
          publishProgress(3, i, music.length);
      }
    }
  }

  private void outputWavFile(File file, short[] music, int samplingRate) throws IOException {
    publishProgress(4, 0, 1);
    
    ByteBuffer buffer = ByteBuffer.allocate(44 + music.length * 2);
    // Struttura di un file wave: https://ccrma.stanford.edu/courses/422/projects/WaveFormat/
    buffer.rewind();
    buffer.order(ByteOrder.LITTLE_ENDIAN);
    
    buffer.put(new byte[] { (byte)'R', (byte)'I', (byte)'F', (byte)'F' });
    buffer.putInt(36 + music.length * 2); // ChunkSize
    buffer.put(new byte[] { (byte)'W', (byte)'A', (byte)'V', (byte)'E' });

    buffer.put(new byte[] { (byte)'f', (byte)'m', (byte)'t', (byte)' ' });
    buffer.putInt(16); // Subchunk1Size
    buffer.putShort((short)1); // AudioFormat
    buffer.putShort((short)1); // NumChannels
    buffer.putInt(samplingRate); // SampleRate
    buffer.putInt(samplingRate * 2); // ByteRate
    buffer.putShort((short)2); // BlockAlign
    buffer.putShort((short)16); // BitsPerSample

    buffer.put(new byte[] { (byte)'d', (byte)'a', (byte)'t', (byte)'a' });
    buffer.putInt(music.length * 2); // Subchunk2Size
    
    buffer.asShortBuffer().put(music); // data
    buffer.position(buffer.position() + music.length * 2);

    DataOutputStream writer = null;
    try {
      writer = new DataOutputStream(new FileOutputStream(file));
      writer.write(buffer.array(), 0, buffer.position());
    } finally {
      try {
        writer.close();
      } catch (Exception ignored) { }
    }
  }
  
  public static short clip(int value) {
    return (short)Math.min(Short.MAX_VALUE, Math.max(Short.MIN_VALUE, value));
  }
}
